Skip to content

feat(api): assign explicit errorCodes to all validators + coverage guard (#231) - #257

Merged
mforce merged 3 commits into
mainfrom
f231-validator-errorcodes
Jul 28, 2026
Merged

mforce merged 3 commits into
mainfrom
f231-validator-errorcodes

Conversation

@mforce

@mforce mforce commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

Closes #231 (follow-up to #45). Assigns a stable Feature.Field.Rule code to every rule in all 34 remaining FluentValidation validators (~220 codes), so the SPA (#182) can key translated validation messages off a machine code with the English message as fallback.

Additive, message-preserving

The errors dictionary is byte-for-byte unchanged — only .WithErrorCode(...) calls were appended; no WithMessage text, rule logic, or When/Unless condition was touched. errorCodes now appears on 400s that previously carried none (it stays absent only for non-FluentValidation 400s).

Verified: build clean · guard 35/35 · Application 105/105 · Integration 427/427.

Naming convention

<Feature>.<Field>.<Rule>, PascalCase, exactly one internal triple so every code contains a . — the discriminator ValidationResponse uses to tell an explicit code from a FluentValidation default (NotEmptyValidator etc.). Decisions:

  • Me.* for self-service (change-own-password, language — mirrors the i18n infrastructure (API half): validation error codes + users.language + GET/PATCH /me #45 Me.Language.Format reference); User.* for admin user ops (distinct: User.Password.* vs Me.NewPassword.*).
  • FarmSettings.* for settings; InventoryAdjustment / InventoryPurchase / FeedUsage / WaterUsage for the inventory movement commands; Payment/SalesOrder/OrderItem for sales; entity noun elsewhere.

The guard (prevents re-drift)

ValidatorErrorCodeCoverageTests reflects over every AbstractValidator<> in Cluckwork.Application and asserts every rule component carries an explicit dotted code — so a new rule added without .WithErrorCode(...) fails the build. Written test-first (it failed listing every uncoded rule, then went green as codes landed). It recurses through child-validator adaptors (RuleForEach(...).ChildRules / .SetValidator) into their leaf rules — an inline ChildRules validator is not a discoverable type, so the recursion (reflecting the adaptor's fixed child validator) is what guards those rules. If a child can't be statically reached it fails closed (flags it), and a dedicated fact asserts a known ChildRules leaf code is actually reached so a future FluentValidation change can't silently reopen the gap. (This recursion + fail-closed hardening — commits after the initial one — resolves the review finding that pi, codex, and the adversarial pass all independently raised.)

Test flip

ErrorCodesContractTests asserted CreateUser-blank-email emits no errorCodes — impossible now that CreateUser is coded. Flipped to assert it emits User.Email.Required. The complementary "no explicit code → errorCodes omitted" branch stays covered as a pure-function case in ValidationResponseTests.Default_framework_code_never_leaks.

Not included

Purely additive on the server; the SPA catalog does not yet map these new codes (English fallback still renders) — mapping them to translated errors keys is downstream #182 work. Unrelated local working-tree edits are not in this branch.

mforce added 3 commits July 28, 2026 01:33
…ard (#231)

Follow-up to #45, which shipped the additive explicit-only errorCodes contract
but coded only the /me/language validator. This assigns a stable
Feature.Field.Rule code to every rule in the remaining 34 FluentValidation
validators (219 rules), so the SPA (#182) can key translated validation
messages off the code with the English message as fallback.

- Additive only: no WithMessage text, rule logic, or When/Unless conditions
  changed — the "errors" dictionary is byte-for-byte unchanged (verified by
  105 Application + 427 integration tests, all green). errorCodes now appears
  on 400s that previously carried none.
- Naming: Feature.Field.Rule, PascalCase, one internal triple so every code
  contains a '.' (the discriminator ValidationResponse uses to tell an explicit
  code from a FluentValidation default). Me.* for self-service (change-own-
  password, language), User.* for admin user ops, FarmSettings.* for settings,
  InventoryAdjustment / InventoryPurchase / FeedUsage / WaterUsage for the
  inventory movements.
- Guard: ValidatorErrorCodeCoverageTests reflects over every AbstractValidator
  in Cluckwork.Application and asserts every rule component carries an explicit
  dotted code — so a new rule added without WithErrorCode fails the build.
  Child-validator adaptors (RuleForEach/ChildRules wrappers) are skipped (they
  emit no message of their own; their leaf rules are coded individually).
- ErrorCodesContractTests flipped: CreateUser blank-email now emits
  User.Email.Required; the "no explicit code -> errorCodes omitted" branch stays
  covered as a pure-function case in ValidationResponseTests.
Review (pi) caught a hole: the coverage guard SKIPPED IChildValidatorAdaptor
components, so the inline RuleForEach(...).ChildRules leaf rules (DailyEntry
Grades[].EggGradeId / .Quantity) were never examined — a new inner rule added
without a code would not have failed the build, defeating #231 for exactly
that pattern.

Recurse THROUGH the adaptor into its fixed child validator (reflected from the
adaptor's private field, since an inline ChildRules validator is not a
discoverable type) rather than skipping it. Provider-based SetValidator(ctx=>…)
has no static instance but those are named AbstractValidator<> types already
discovered independently. Cycle-guarded.

Verified by mutation: removing a child rule's WithErrorCode now fails the guard
with "Quantity -> ''"; it passed before. Guard still 35/35 green with all codes
present.
…idators (#231)

Addresses codex + adversarial review of the ChildRules recursion:

- Fail closed: if GetChildValidator cannot reach an adaptor's child (a
  provider-based SetValidator(ctx=>...), or a FluentValidation change that
  breaks the private-field reflection), emit an uncoded sentinel instead of
  silently continuing — so an un-inspectable child can never quietly hide
  uncoded rules. None exist today, so the guard stays 36/36 green.
- Fail loud: a new fact asserts a known inline-ChildRules leaf code is actually
  reached, so if a FluentValidation upgrade renames the private field the
  recursion depends on, the guard fails here (and via fail-closed in the
  DailyEntry theory cases) instead of silently reopening the #231 hole.
@mforce

mforce commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

Review — codex + 2 Claude agents + pi (all findings resolved)

Reviewer Verdict Notes
pi ✅ (after fix) Approved codes/naming/placement; caught the one real issue
Claude (spec/additivity) ✅ approve additive-only, 170 unique 2-dot codes, naming coherent
codex ✅ (after fix) approved everything except the guard hardening below
Claude (adversarial) ✅ approve ruled out placement/When-scope/dup-code/contract-flip/instantiation at high confidence

One substantive finding — independently raised by all three of pi, codex, and the adversarial pass, then fixed in follow-up commits:

  • The coverage guard originally skipped IChildValidatorAdaptor, so the inline RuleForEach(...).ChildRules leaf rules in RecordDailyEntry/AdjustDailyEntry (Grades[].EggGradeId / .Quantity) were never examined — a future uncoded inner rule wouldn't have failed the build, defeating i18n: assign explicit errorCodes to the remaining 30 validators #231 for that pattern. (No live bug: those leaves were coded.)
  • Fix (38f47ee): recurse through the adaptor into its child validator (reflecting the fixed inline validator, since it's not a discoverable type). Mutation-proven — stripping a child-rule code now fails the guard (Quantity -> ''); it passed before.
  • Hardening (882685e): per codex, fail closed when a child can't be statically reached (provider-based SetValidator(ctx=>…) or a FluentValidation change breaking the reflection) — emit an uncoded sentinel rather than silently pass. Per the adversarial fragility note, a fail-loud fact asserts a known ChildRules leaf code is actually reached, so a future FV bump can't silently reopen the gap.

Ruled out by the adversarial pass (all confirmed against source): every .WithErrorCode on the correct component (before .When, per-component in shared conditions); .When/.Unless scope unchanged; no duplicate/wrong codes; blank-email fires both User.Email.Required + User.Email.Format (index-aligned, Required first — the flipped test asserts Required, robust); all 35 validators single-ctor so instantiation is safe.

Final state: additive-only (errors unchanged) · guard 36/36 (35 theory + 1 fail-loud fact) · Application 106/106 · Integration 427/427 · every code 3-part dotted.

Non-blocking note (codex): branch name f231-... follows this repo's actual f<issue>-... practice (the whole #182 sweep used it) rather than the feat/… doc convention — left as-is for consistency with history.

@mforce
mforce merged commit f13f30d into main Jul 28, 2026
7 checks passed
@mforce
mforce deleted the f231-validator-errorcodes branch July 28, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant